动态规划实现O(n)爆破任意加密
0x00 前言
这个框架是当你手上有一个加密,但是不知道他的实现的时候(比方说要通过逆向来获得),已知密文,爆破出明文。
0x01 前提
其实这个有点标题党,因为并不能爆破任意加密,像AES、DES这样的分块加密就不行......而像魔改base64、魔改rc4这种加密就随便爆破了,所以条件是需要有从左到右的对应的关系,顺序不能被打乱。
当然分块加密后续也会支持,这个以后再说吧......
0x02 实例
接下来我会用一道CTF题目的实例,这是一个魔改base64+魔改rc4的加密的题目,但是不用去逆向他,直接用框架爆破。
EXE变成DLL
这个是我学PE的时候就想到的,应该有其他人早就玩过类似的东西了。但是我在网上找却没找到过类似的思路,这里就写一下吧。
首先EXE和DLL都是PE文件格式,所以当我们逆向一个EXE的时候,需要分析他其中一个函数。这个时候可能就会需要call这个函数以便详细分析,但跨进程远程call明显就比较麻烦,所以我就想到反正都是PE文件格式,只要把characteristic那个bit改成1就是DLL了,那我们不就可以LoadLibrary把这个EXE当做一个DLL加载到自己的内存空间中了吗?
然后虽然并没有导出表,所以并不能通过GetProcAddress获取到我们要分析的函数的地址,但是LoadLibrary返回的值就是模块的基址,这个时候加上函数的RVA(通过IDA逆向分析获得),就可以直接call它了!
具体操作如图所示:
首先是把入口点改为0,不然LoadLibrary会直接调用他的主函数,这不是我们所期望的。还有就是在特征值(characteristic)里面把DLL勾选了,不然重定位会出问题,我不知道为什么,有大神知道的话可以解答一下。。然后把文件后缀改成dll(这个好像改不改都行),就可以当dll使了。
程序逆向分析
这个CrackMe是让你输入,然后把加密结果写到文件里,题目要求写入文件的内容为"Itl9qnxD/IJhoarL"。程序main函数如下。
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *v3; // eax
const char *v4; // eax
FILE *v5; // eax
FILE *v6; // esi
const void *v7; // ecx
void *v9; // [esp+Ch] [ebp-260h]
int v10; // [esp+10h] [ebp-25Ch]
int v11; // [esp+14h] [ebp-258h]
void *v12; // [esp+18h] [ebp-254h]
int v13; // [esp+1Ch] [ebp-250h]
int v14; // [esp+20h] [ebp-24Ch]
void *Str; // [esp+24h] [ebp-248h]
size_t Size; // [esp+34h] [ebp-238h]
unsigned int v17; // [esp+38h] [ebp-234h]
char *Filename; // [esp+3Ch] [ebp-230h]
int v19; // [esp+4Ch] [ebp-220h]
unsigned int v20; // [esp+50h] [ebp-21Ch]
char Buffer; // [esp+54h] [ebp-218h]
char Dst; // [esp+55h] [ebp-217h]
int v23; // [esp+268h] [ebp-4h]
Buffer = 0;
memset(&Dst, 0, 0x1FFu);
v3 = sub_401900(std::cout);
std::basic_ostream<char,std::char_traits<char>>::operator<<(v3, std::endl);
//输出废话,这个我们到时候会通过hook成空函数取消掉,不然输出会让爆破很慢
gets(&Buffer);
//这个到时候会hook成我们自己的函数,并把当前所遍历的input放进他的缓冲区,待会会有详细解释
v12 = 0;
v13 = 0;
v14 = 0;
sub_401B50(&Buffer, (int)(&Buffer + strlen(&Buffer)));
v23 = 0;
v9 = 0;
v10 = 0;
v11 = 0;
sub_401000((int)&v9, (int)&v12);
LOBYTE(v23) = 1;
sub_401B20((int)&Str, (int)&v9);
LOBYTE(v23) = 2;
sub_401260(&Filename);
v4 = (const char *)&Filename;
if ( v20 >= 0x10 )
v4 = Filename;
//上面就是各种加密了,我们不用去逆向他
v5 = fopen(v4, "wb+");
v6 = v5;
if ( v5 )
{
v7 = &Str;
if ( v17 >= 0x10 )
v7 = Str;
fwrite(v7, Size, 1u, v5);
fclose(v6);
}
//这些文件操作也都会被hook掉,除了fwrite其他都会变成空函数,fwrite会调用某个函数做一些操作,待会详细讲
if ( v20 >= 0x10 )
operator delete(Filename);
v20 = 15;
v19 = 0;
LOBYTE(Filename) = 0;
if ( v17 >= 0x10 )
operator delete(Str);
v17 = 15;
Size = 0;
LOBYTE(Str) = 0;
if ( v9 )
operator delete(v9);
if ( v12 )
operator delete(v12);
system("pause");//这个也会hook成空函数
return 0;
}
所以就是吃一手hook,我这里基本上用的是IAT hook,有一个e8的call我是直接用的改参数值hook。
IAT hook就是要拿到IAT的RVA,这个简单,IDA双击那个函数,就是IAT地址,去掉400000就是RVA了,如图所示:
框架使用
现在来看看怎么去用这个框架:
#include <stdio.h>
#include <windows.h>
#include <string.h>
#include <vector>
#include "BruteForce.h"
#include "WindowsHookFramework.h"
typedef int(*main_t)();
#define GETS_IAT 0x40DC
#define MAIN_DISPL 0x13E0
#define FWRITE_IAT 0x4060
#define SYSTEM_IAT 0x4068
#define FOPEN_IAT 0x406C
#define FCLOSE_IAT 0x4064
#define COUT_IAT 0x4038
#define OUTPUT_STR_E8ARG 0x1445
#define KEY "Itl9qnxD/IJhoarL"
#define INPUT_LEN 12
main_t hismain;
class CrackCtf : public BruteForce
{
public:
CrackCtf(size_t inputLen, const bfbyte* answer, size_t answerLen)
:BruteForce(inputLen, answer, answerLen) {}
~CrackCtf();
private:
virtual void doEncode() override
{//继承重写doEncode函数,他必须要通过getInput获取到当前输入,然后把这个输入加密,再把加密结果作为参数调用testEncodeResult
//this function must call getInput to get the input, encode it,
//and call testEncodeResult with the result of encoding
hismain();//调用他的主函数,这个时候主函数已经被各种hook了
}
public:
static size_t __cdecl myfwrite(char *str, size_t Size, size_t Count, FILE *File);
};
CrackCtf::~CrackCtf() {}
CrackCtf crack(INPUT_LEN, (const bfbyte*)KEY, strlen(KEY));
//本应该是单例模式,但是CTF比赛的时间不可能给你写单例
//第一个参数是所需要破解的最大长度,这个我是自己试出来的(看多长的输入才能跟要求的答案一样长并且没有等于号(因为这是魔改base64)。。。
//后面两个参数是密文和密文的长度
size_t CrackCtf::myfwrite(char * str, size_t Size, size_t Count, FILE * File)
{
//printf("%s\n", str);
if (crack.testEncodeResult((bfbyte*)str))
{//调用testEncodeResult,str就是加密结果
//如果返回true,说明爆破出了正确答案,这个时候弹出当前输入
bfbyte buf[INPUT_LEN + 1];
memset(buf, 0, INPUT_LEN + 1);
crack.getInput(buf, INPUT_LEN + 1);
MessageBoxA(NULL, (LPCSTR)buf, "flag", MB_OK);
}
return 0;
}
char* __cdecl mygets(char* buffer)
{
crack.getInput((bfbyte*)buffer);
//get当前的Input,并且塞到缓冲区里面
return buffer;
}
DECLARE_EMPTY_FUNC(emptyfunc, 0)
DECLARE_EMPTY_FUNC(emptyfunc2, 4)
int main()
{
HMODULE pBase = LoadLibraryA("Encrypt messages.dll");
//Encrypt messages is an exe crack me in one CTF competition
//change the characteristic to dll, and set the entry address to 0, it can become a dll
hookIAT(pBase, GETS_IAT, (func_p_t)&mygets);
hookIAT(pBase, FWRITE_IAT, (func_p_t)&CrackCtf::myfwrite);
hookIAT(pBase, SYSTEM_IAT, (func_p_t)&emptyfunc);
hookIAT(pBase, FOPEN_IAT, (func_p_t)&emptyfunc);
hookIAT(pBase, FCLOSE_IAT, (func_p_t)&emptyfunc);
hookIAT(pBase, COUT_IAT, (func_p_t)&emptyfunc2);
hookE8Call(pBase, OUTPUT_STR_E8ARG, (func_p_t)&emptyfunc);
//hook IAT 和 e8 call
hismain = (main_t)((PBYTE)pBase + MAIN_DISPL);
//算出Main函数地址
crack.startCrack();
//调用startCrack,开始爆破
return 0;
}
基本秒破,如图:
0x03 后言
嗯......基本上是这样,目前是通过继承和纯虚函数实现的,以后可能会改成模板......现在暂不支持分块加密,以后可能会支持,也有可能会支持多线程,这个以后再说吧......
源码我放在github上了:
https://github.com/Mem2019/BruteForceCrackEncoding
代码基本上是用C with class写的。没怎么用C++的特性,考虑到是爆破,就没有用C++的那套理论,怕慢。代码写的渣,大神轻喷......
0x04 更新:已支持分块加密爆破
容我瞎扯唠叨两句
我本来以为,支持分块加密会需要比较复杂的算法,可能需要加很多新代码,我之前想到的算法也的确如此。但是今天突然抖了个机灵,想到一个又简单又有效的算法,只改了几行,就支持了分块加密爆破。。。主要就是当progress大于块大小的倍数时再更新,跟之前算法区别不大,思想基本一样。。。改完之后测试通过了搞得我自己都不敢相信日常写fragile的垃圾代码的我能够改几行就加一个新功能,哈哈......
写了个简单的AES爆破demo
嗯,大概就是不用AES的解密算法,不知道秘钥(但是秘钥理论上能被逆出来)的情况下爆破AES.....其实这个又标题党了,我这个分块加密其实就是一块一块地爆破而不是之前那样一字节一字节地爆破,而AES一块就是128个bit,如果爆破起来真的很慢。(以后可能会用线程池实现多线程加速)
所以我这个又实现了一个自定义字符集的功能,之前会从把从''\x00'到'\xff'都爆破一次,现在可以自定义字符集。。比方说我知道原文只有'\x41'和'\x00',我就可以在参数里面指定,然后爆破的时候只去遍历这两个字符,这样勉强能爆破128位分块的AES。
代码如下:
#include "BruteForce.h"
#include "aes.h"
#include <stdio.h>
#include <stdlib.h>
uint8_t key[] = {
0x00, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b,
0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13,
0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1a, 0x1b,
0x1c, 0x1d, 0x1e, 0x1f };
uint8_t res[] =
{ 0x35,0x27,0xbd,0x7d,0x7f,0xfe,0x7e,0xdb,0xd2,0x7f,0xf9,0xc9,0x11,0x88,0x43,0x2a,
0xcf,0xf1,0xf9,0x47,0xec,0xd4,0xf6,0x0e,0xf6,0xa6,0x2a,0x3c,0x3f,0x1c,0xba,0xe3 };
void printHex(bfbyte* input, size_t inputLen)
{
for (size_t i = 0; i < inputLen; i++)
{
printf("%02x ", input[i]);
}
printf("\n");
}
class CrackAes : public BruteForce
{
public:
CrackAes(size_t inputLen, const bfbyte* answer, size_t answerLen)
:BruteForce(inputLen, answer, answerLen, AES_BLOCK_SIZE, (bfbyte*)"A", 2)
{
this->outputBufLen = answerLen - answerLen % AES_BLOCK_SIZE + AES_BLOCK_SIZE;
this->inputBufLen = inputLen;
}
~CrackAes() {};
private:
size_t outputBufLen;
size_t inputBufLen;
virtual void doEncode() override
{//继承重写doEncode函数,他必须要通过getInput获取到当前输入,然后把这个输入加密,再把加密结果作为参数调用testEncodeResult
//this function must call getInput to get the input, encode it,
//and call testEncodeResult with the result of encoding
uint8_t* input = new uint8_t[inputBufLen];
uint8_t* output = new uint8_t[outputBufLen];
size_t inputLen = getInput(input);
if (rand() % 4096 == 0)
{
printHex(input, inputLen);
}
if (!aesEncrypt(input, inputLen, output, outputBufLen, key))
{
#ifdef _DEBUG
throw 0;
#endif // DEBUG
}
if (this->testEncodeResult(output))
{
printHex(input, inputLen);
system("pause");
}
delete[] output;
delete[] input;
}
};
int main()
{
CrackAes aes(19, res, AES_BLOCK_SIZE * 2);
aes.startCrack();
/*
output:
41 00 00 00 41 41 41 41 00 00 41 41
41 41 00 41 41 00 41 00 41 41 00 00 00
41 41 41 00 41 41 00 41 41 41 00 00 41 41
41 00 00 00 41 41 41 00 00 41 00 00 00 00
00 00 00 41 00 41 00 41 00 41 41 41 00 00
00 41 41 41 00 41 00 41 41 00 41 41 00 00 41
41 41 00 00 00 00 00 00 41 00 41 00 41 00 41
41 41 41 00 00 00 41 41 00 41 00 00 00 41 41
00 41 00 00 41 00 00 41 00 41 00 00 41 00 00
41 00 41 00 41 41 00 00 41 41 00 00 41 00 00
00 41 00 00 00 00 00 41 00 41 00 00 00 41 00
00 41 41 00 41 00 41 41 41 00 41 00 00 41 00
41 00 41 00 41 41 41 00 00 00 41 41 00 41 00
41 00 00 41 41 41 41 41 00 00 41 41 41 41 00
00 41 41 41 00 41 00 00 41 00 41 41 00 41 41 00 00 41
请按任意键继续. . .
00 41 41 41 00 41 00 00 41 00 41 41 00 41 41 00 00 41 00
请按任意键继续. . .
*/
return 0;
}
本文由看雪论坛 holing 原创
转载请注明来自看雪社区
热门阅读
点击阅读原文/read,
更多干货等着你~